"""
A Go Board
"""
from pygo1963.model.Constants import EMPTY
from pygo1963.goLogic.GoUtils import get_vertex_around_position,\
    get_opponent_color
from pygo1963.model.Group import Group
import itertools
from pygo1963.model.Constants import BLACK_COLOR, WHITE_COLOR
from pygo1963.model.Move import Move
from pygo1963.model.Vertex import Vertex
import hashlib


class Board:
    """
    Model class that represents a Go Board.
    """
    # Class members
    #
    # game_finished
    # winner
    # size
    # content
    # move_history
    # groups
    
    def __init__(self, size):
        self.game_finished = False
        self.last_black_move_is_pass = False
        self.last_white_move_is_pass = False
        self.winner = None
        self.size = size
        self.content = [[EMPTY for _ in range(size)] for _ in range(size)]    
        self.move_history = []
        self.groups_history = []
        self.groups = []
    
    def make_move(self, move):
        """
        Makes the specified move on the board. If it's an invalid move, 
        an InvalidMoveError is raised.
        If the game is finished after the move -i.e. it is a capture- then
        self.game_finished is set to True, and the color of the winner 
        (if any) is set on self.winner
        """
        
        if Vertex.PASS() == move.vertex:
            self.push_history(move)
            self.__set_winner_if_possible(move)
            
            if move.color == WHITE_COLOR:
                self.last_white_move_is_pass = True
            else:
                self.last_black_move_is_pass = True
            return
        
        if move.color == WHITE_COLOR:
            self.last_white_move_is_pass = False
        else:
            self.last_black_move_is_pass = False
        
        x, y = move.vertex.x, move.vertex.y
        
        if self.content[x][y] != EMPTY:
            raise InvalidMoveError
        
        # Make move
        self.content[x][y] = move.color
        self.push_history(move)
        self.__add_to_group(move)
        # Update Liberties of the other groups
        self.__remove_liberties(move)
        self.__set_winner_if_possible(move)

    def push_history(self, move):
        """
        Stores the board information, including groups
        """
        
        groups_copy = [group.copy(self) for group in self.groups]

        self.move_history.append(move)
        self.groups_history.append(groups_copy)
        
    def pull_history(self):
        """
        Restores the board to the previous position, including groups
        """
            
        last_move = self.move_history.pop()
        self.groups = self.groups_history.pop()
        
        self.game_finished = False
        self.winner = None
        
        if last_move.vertex == Vertex.PASS():
            if last_move.color == WHITE_COLOR:
                self.last_white_move_is_pass = False
            else:
                self.last_black_move_is_pass = False
        else: 
            x, y = last_move.vertex.x, last_move.vertex.y
            self.content[x][y] = EMPTY

    def undo_move(self):
        """
        Undo the last move. 
        """
        self.pull_history()

    def get_group(self, vertex):
        """
        Gets the group which has the specific vertex
        """

        for group in self.groups:
            if vertex in group.stones:
                return group
            
        return None    

    def get_groups_by_color(self, color):
        """
        Returns the groups of a specific color
        """
        return [group for group in self.groups if group.color == color]

    def __str__(self):
        """
        String representation of the board.
        """

        columns = "ABCDEFGHJKLMNOPQRST"
        SPACE = " "

        board = "\n"
        board += SPACE * 3 + SPACE.join(columns[0:self.size]) + "\n"
        
        for x in range(self.size - 1, -1, -1):
            
            row = str(x + 1)
            
            if len(row) == 1:
                board += SPACE + row + SPACE
            else:
                board += row + SPACE

            for y in range(self.size):
                board += self.content[y][x] + SPACE
                
            if self.size > 9 and len(row) == 1:
                board += SPACE 

            board +=  row  + "\n"

        board += SPACE * 3 + SPACE.join(columns[0:self.size])
        
        return board    
    
    def __set_winner_if_possible(self, move):

        color = move.color
        
        if Vertex.PASS() == move.vertex:
            if ((color == WHITE_COLOR and self.last_white_move_is_pass) or 
                (color == BLACK_COLOR and self.last_black_move_is_pass)):
                self.winner = get_opponent_color(color)
                self.game_finished = True
        elif not self.__has_liberties(get_opponent_color(color)):
            # capture opponent
            self.winner = color
            self.game_finished = True
        elif not self.__has_liberties(color): 
            # suicide
            self.winner = get_opponent_color(color)
            self.game_finished = True
        
    def __has_liberties(self, color):
        
        for group in self.get_groups_by_color(color):
            if group.is_captured():
                return False
            
        return True        
   
    def __add_to_group(self, move):
        """
        Adds the stone to the corresponding group.
        If no group is near, it creates a new group. 
        If the stone can be added to more than one group, the groups have to be fusioned 
        """
        vertex_list = get_vertex_around_position(self.size, move.vertex)
        color = move.color
        new_stone = move.vertex
        near_groups = []
        
        for vertex in vertex_list:
            x, y = vertex.x, vertex.y
            if self.content[x][y] == color:
                near_groups.append(self.get_group(vertex))

        if not near_groups:
            # Creates new group
            self.groups.append(Group(self, color, new_stone))
        elif len(near_groups) == 1:
            # Adds stone to the group
            near_groups[0].add_stone(self, new_stone)
        else:
            # Fusion groups
            new_group = Group(self, color, new_stone)
            stone_list = []

            for group in near_groups:
                if group in self.groups:
                    stone_list += group.stones
                    self.groups.remove(group)

            for stone in set(stone_list):
                new_group.add_stone(self, stone)

            self.groups.append(new_group)
 
    def __remove_liberties(self, move):
        """
        Updates the liberties of all opponent groups when a stone is added, 
        removing the corresponding libertie
        """
        for group in self.get_groups_by_color(get_opponent_color(move.color)):
            group.remove_libertie(move.vertex)
    
    def as_string(self):
        flat = ''.join(itertools.chain(*self.content))
        
        m = hashlib.md5()
        m.update(flat)
        
        return m.digest()
    
    def as_move_list(self):
        """ 
        Returns the state of the board as a list of moves (unordered) to reproduce it. 
        """
        moves = []
        
        for x in range(self.size):
            for y in range(self.size):
                if self.content[x][y] == BLACK_COLOR:
                    moves.append(Move(BLACK_COLOR, Vertex(x, y)))
                elif self.content[x][y] == WHITE_COLOR:
                    moves.append(Move(WHITE_COLOR, Vertex(x, y)))
        
        return moves
        
    
    def get_pieces(self):
        """ 
        Returns two list of tuples, for the white and black pieces coordinates.
        """
        
        whites = []
        blacks = []
        
        for x in range(self.size):
            for y in range(self.size):
                if self.content[x][y] == BLACK_COLOR:
                    blacks.append((x, y))
                elif self.content[x][y] == WHITE_COLOR:
                    whites.append((x, y))
        
        return whites, blacks    
    
    def copy(self):
        """ Returns a new board with the same state as this. """
        
        new_board = Board(self.size)
        new_board.last_black_move_is_pass = self.last_black_move_is_pass
        new_board.last_white_move_is_pass = self.last_white_move_is_pass
        
        #copy the content
        for i in range(self.size):
            new_board.content[i] = list(self.content[i])    
        
        #copy the groups
        for group in self.groups:
            new_board.groups.append(group.copy(self))
        
        return new_board
        
        
class InvalidMoveError(Exception):
    """
    Exception that arises when a move is illegal.
    """
    pass
